<!doctype html>
<!--
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<html>
<head>
  <meta charset="utf-8">
  <script src="../../node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
  <script src="wct-browser-config.js"></script>
  <script src="../../node_modules/wct-browser-legacy/browser.js"></script>
  <script type="module" src="../../polymer-legacy.js"></script>
  <script type="module" src="./property-effects-elements.js"></script>
<body>
<dom-repeat id="class-repeat" items='["class1", "class2"]'>
  <template>
    <div class$=[[item]] clazz$="[[item]]">[[item]]</div>
  </template>
</dom-repeat>

<script type="module">
import './property-effects-elements.js';
import { Polymer, html } from '../../polymer-legacy.js';
import { setSanitizeDOMValue, sanitizeDOMValue, getSanitizeDOMValue, setLegacyWarnings, legacyUndefined, setLegacyUndefined, setOrderedComputed, orderedComputed } from '../../lib/utils/settings.js';
import { PropertyEffects } from '../../lib/mixins/property-effects.js';
import { flush } from '../../lib/utils/flush.js';

setLegacyUndefined(Boolean(window.location.search.match('legacyUndefined')));
setOrderedComputed(Boolean(window.location.search.match('orderedComputed')));

suite('single-element binding effects', function() {

  var el;

  setup(function() {
    el = document.createElement('x-basic');
    document.body.appendChild(el);
  });

  teardown(function() {
    document.body.removeChild(el);
  });

  test('undefined input value', function() {
    assert.equal(el.$.boundInput.value, '', 'undefined input value not blank');
    el.text = 'this is a test';
    assert.equal(el.$.boundInput.value, 'this is a test', 'binding to input didn\'t go');
    el.text = undefined;
    assert.equal(el.$.boundInput.value, '', 'undefined input value not blank');
  });

  test('undefined textarea value', function() {
    assert.equal(el.$.boundTextArea.value, '', 'undefined textarea value not blank');
    el.text = 'this is a test';
    assert.equal(el.$.boundTextArea.value, 'this is a test', 'binding to textarea didn\'t go');
    el.text = undefined;
    assert.equal(el.$.boundTextArea.value, '', 'undefined textarea value not blank');
  });

  test('id is bindable', function() {
    assert.equal(el.root.querySelector('span[idtest]').id, 'span', 'id bound to <span> not found');
  });

  test('textContent binding updates', function() {
    el.text = 'this is a test';
    assert.equal(el.$.boundText.textContent, 'this is a test', 'Value not propagated to textContent');
  });

  test('textContent binding to undefined is empty string', function() {
    el.text = 'this is a test';
    el.text = undefined;
    assert.equal(el.$.boundText.textContent, '', 'undefined bound to textContent should be empty string');
  });

  test('textContent binding to null is empty string', function() {
    el.text = null;
    assert.equal(el.$.boundText.textContent, '', 'null bound to textContent should be empty string');
  });

  test('textContent binding to zero is empty correct', function() {
    el.text = 0;
    assert.equal(el.$.boundText.textContent, '0', 'zero bound to textContent should be empty string');
  });

  test('camel-case binding updates', function() {
    el.value = 41;
    assert.equal(el.$.boundChild.camelCase, 41, 'Value not propagated to camelCase property');
  });

  test('annotation binding updates', function() {
    el.value = 42;
    assert.equal(el.$.boundChild.value, 42, 'Value not propagated to bound child');
  });

  test('negated annotation binding updates', function() {
    el.bool = true;
    assert.equal(el.$.boundChild.negvalue, false, 'Value not negated');
    el.bool = false;
    assert.equal(el.$.boundChild.negvalue, true, 'Value not negated');
  });

  test('observer called', function() {
    assert.equal(el.observerCounts.valueChanged, 1, 'observer not called once for default value at configure');
    el.value = 43;
    assert.equal(el.observerCounts.valueChanged, 2, 'observer not called after property change');
  });

  test('observer called only once for null', function() {
    el.clearObserverCounts();
    assert.equal(el.observerCounts.valueChanged, 0);
    el.value = {};
    assert.equal(el.observerCounts.valueChanged, 1);
    el.value = null;
    assert.equal(el.observerCounts.valueChanged, 2);
    el.value = null;
    assert.equal(el.observerCounts.valueChanged, 2);
    el.value = {};
    assert.equal(el.observerCounts.valueChanged, 3);
    el.value = null;
    assert.equal(el.observerCounts.valueChanged, 4);
    el.value = null;
    assert.equal(el.observerCounts.valueChanged, 4);
  });

  test('computed value updates', function() {
    el.value = 44;
    assert.equal(el.computedvalue, 45, 'Computed value not correct');
    assert.equal(el.$.boundChild.computedvalue, 45, 'Computed value not propagated to bound child');
  });

  test('computed value readOnly from imperative set', function() {
    el.value = 44;
    // Should have no effect
    el.computedvalue = 99;
    assert.equal(el.computedvalue, 45, 'Computed value not correct');
    assert.equal(el.$.boundChild.computedvalue, 45, 'Computed value not propagated to bound child');
  });

  test('computed values to same method updates', function() {
    el.value = 44;
    el.valuetwo = 144;
    assert.equal(el.computedvalue, 45, 'Computed value not correct');
    assert.equal(el.computedvaluetwo, 145, 'Computed value not correct');
    assert.equal(el.$.boundChild.computedvalue, 45, 'Computed value not propagated to bound child');
  });

  test('computed value using computing fn from behavior', function() {
    el.value = 44;
    assert.equal(el.$.boundChild.computedFromBehavior, 'computed:44');
    el.computeFromBehavior = function(value) {
      return 'new:' + value;
    };
    assert.equal(el.$.boundChild.computedFromBehavior, 'new:44');
  });

  test('notification sent', function() {
    var notified = 0;
    el.addEventListener('notifyingvalue-changed', function(e) {
      assert.equal(e.detail.value, 45);
      notified++;
    });
    el.addEventListener('camel-notifying-value-changed', function(e) {
      assert.equal(e.detail.value, 45);
      notified++;
    });
    el.notifyingvalue = 45;
    el.camelNotifyingValue = 45;
    assert.equal(notified, 2, 'Notification events not sent');
  });

  test('computed observer called', function() {
    el.clearObserverCounts();
    el.value = 46;
    assert.equal(el.observerCounts.computedvalueChanged, 1, 'observer not called');
  });

  test('NaN does not loop observers', function() {
    el.clearObserverCounts();
    el.addEventListener('notifierwithoutcomputing-changed', function() {
      if (el.observerCounts.notifierWithoutComputingChanged >= 3) {
        throw new Error('infinite loop!');
      }
    });
    el.notifierWithoutComputing = NaN;
    assert.equal(el.observerCounts.notifierWithoutComputingChanged, 1,
                 'NaN was not handled as expected');
  });


  test('computed notification sent', function() {
    var notified = 0;
    el.addEventListener('computednotifyingvalue-changed', function(e) {
      assert.equal(e.detail.value, 49);
      notified++;
    });
    el.notifyingvalue = 47;
    assert.equal(notified, 1, 'Notification event not sent');
  });

  test('computed property with multiple dependencies', function() {
    var notified = 0;
    el.addEventListener('computed-from-multiple-values-changed', function() {
      notified++;
    });
    el.sum1 = 10;
    el.sum2 = 20;
    el.divide = 2;
    assert.equal(el.computedFromMultipleValues, 15, 'Computed value wrong');
    assert.equal(notified, 1, 'Notification event not sent');
    assert.equal(el.observerCounts.computedFromMultipleValuesChanged, 1, 'observer not called');
  });

  test('computed annotation with literals', function() {
    el.bool = true;
    assert.equal(el.$.boundChild.computedFromMixedLiterals, '3foo', 'Wrong result from mixed literal arg computation');
    assert.equal(el.$.boundChild.computedFromPureLiterals, '3foo', 'Wrong result from pure literal arg computation');
    assert.equal(el.$.boundChild.computedFromTrickyFunction, '3foo', 'Wrong result from tricky function with pure literal arg computation');
    assert.equal(el.$.boundChild.computedFromTrickyLiterals, '3tricky,\'zot\'', 'Wrong result from tricky literal arg computation');
    assert.equal(el.$.boundChild.computedFromTrickyLiterals2, '3tricky,\'zot\'', 'Wrong result from tricky literal arg computation');
    assert.equal(el.$.boundChild.computedFromTrickyLiterals3, '3tricky,\'zot\'', 'Wrong result from tricky literal arg computation');
    assert.equal(el.$.computedContent.textContent, '3tricky,\'zot\'', 'Wrong textContent from tricky literal arg computation');
    assert.equal(el.$.computedContent2.textContent, '(3', 'Wrong textContent from tricky literal arg computation');
  });

  test('computed annotation with no args', function() {
    assert.equal(el.$.boundChild.computedFromNoArgs, 'no args!', 'Wrong content when computed has no args');
  });

  test('no read-only observer called with assignment', function() {
    el.readonlyvalue = 46;
    assert.equal(el.observerCounts.readonlyvalueChanged, 0, 'observer should not be called for readOnly prop assignment');
  });

  test('read-only observer called with _setReadonlyvalue', function() {
    el._setReadonlyvalue(46);
    assert.equal(el.observerCounts.readonlyvalueChanged, 1, 'observer should be called');
    assert(el.readonlyvalue == 46, 'value should be changed but was not');
  });

  test('no read-only notification sent with assignment', function() {
    var notified = 0;
    el.addEventListener('readonlyvalue-changed', function() {
      notified++;
    });
    el.readonlyvalue = 47;
    assert.equal(notified, 0, 'Notification should not be called for readOnly prop assignment');
  });

  test('read-only notification sent with _setReadonlyvalue', function() {
    var notified = 0;
    el.addEventListener('readonlyvalue-changed', function(e) {
      assert.equal(e.detail.value, 47);
      notified++;
    });
    el._setReadonlyvalue(47);
    assert.equal(notified, 1, 'Notification event not sent');
  });

  test('multiple dependency observer called once', function() {
    el.setProperties({
      dep1: true,
      dep2: {},
      dep3: 42
    });
    assert.equal(el.observerCounts.multipleDepChangeHandler, 1, 'observer not called once');
  });

  test('setProperties does not set readOnly by default', function() {
    assert.equal(el.observerCounts.valueChanged, 1);
    assert.equal(el.observerCounts.readonlyvalueChanged, 0);
    el.setProperties({
      value: 'shouldChange',
      nofx: 'shouldChange',
      readonlyvalue: 'shouldNotChange'
    });
    assert.equal(el.value, 'shouldChange');
    assert.equal(el.nofx, 'shouldChange');
    assert.equal(el.readonlyvalue, undefined);
    assert.equal(el.observerCounts.valueChanged, 2);
    assert.equal(el.observerCounts.readonlyvalueChanged, 0);
  });

  test('setProperties sets readOnly using `setPrivate` arg', function() {
    assert.equal(el.observerCounts.valueChanged, 1);
    assert.equal(el.observerCounts.readonlyvalueChanged, 0);
    el.setProperties({
      value: 'shouldChange',
      nofx: 'shouldChange',
      readonlyvalue: 'shouldChange'
    }, true);
    assert.equal(el.value, 'shouldChange');
    assert.equal(el.nofx, 'shouldChange');
    assert.equal(el.readonlyvalue, 'shouldChange');
    assert.equal(el.observerCounts.valueChanged, 2);
    assert.equal(el.observerCounts.readonlyvalueChanged, 1);
  });

  test('annotated computed property', function() {
    el.value = 20;
    el.add = 40;
    el.divide = 3;
    assert.equal(el.$.boundChild.computedInline, 20, 'computedInline not correct');
    assert.equal(el.$.boundChild.computedInline2, 20, 'computedInline2 not correct');
    assert.equal(el.$.boundChild.computedInline3, 20, 'computedInline3 not correct');
    assert.equal(el.$.boundChild.negComputedInline, false, 'negComputedInline not correct');
  });

  test('annotated computed attribute', function() {
    el.value = 20;
    el.add = 40;
    el.divide = 3;
    assert.equal(el.$.boundChild.getAttribute('computedattribute'), 20, 'computed attribute not correct');
    assert.equal(el.$.boundChild.getAttribute('computedattribute2'), 20, 'computed attribute not correct');
  });

  test('annotated style attribute binding', function() {
    el.boundStyle = 'padding: 37px;';
    assert.equal(getComputedStyle(el.$.boundChild).paddingTop, '37px', 'style attribute binding not correct');
  });

  test('annotated dataset attribute binding', function() {
    if (el.$.boundChild.datast) {  // IE10, sigh
      el.dataSetId = 'yeah';
      assert.equal(el.$.boundChild.dataset.id, 'yeah', 'dataset.id dataset property not set correctly');
      assert.equal(el.$.boundChild.getAttribute('data-id'), 'yeah', 'data-id attribute not set correctly');
    }
  });

  test('custom notification event to property', function() {
    el.$.boundChild.customEventValue = 42;
    el.fire('custom', null, {node: el.$.boundChild});
    assert.equal(el.customEventValue, 42, 'custom bound property incorrect');
    assert.equal(el.observerCounts.customEventValueChanged, 1, 'custom bound property observer not called');
  });

  test('custom notification event to path', function() {
    el.clearObserverCounts();
    el.$.boundChild.customEventObjectValue = 84;
    el.$.boundChild.dispatchEvent(new Event('change'));
    assert.equal(el.customEventObject.value, 84, 'custom bound path incorrect');
    assert.equal(el.observerCounts.customEventObjectValueChanged, 1, 'custom bound path observer not called');
  });

  test('custom notification bubbling event to property', function() {
    const child = document.createElement('div');
    el.$.boundChild.appendChild(child);

    el.$.boundChild.customEventValue = 42;
    child.dispatchEvent(new Event('custom', {bubbles: true}));
    assert.equal(el.customEventValue, 42, 'custom bound property incorrect');
    assert.equal(el.observerCounts.customEventValueChanged, 1, 'custom bound property observer not called');
  });

  test('computed property with negative number', function() {
    assert.equal(el.$.boundChild.computedNegativeNumber, -1);
  });

  test('computed property with negative literal', function() {
    assert.equal(el.$.boundChild.computedNegativeLiteral, undefined);
  });

  test('computed binding with wildcard', function() {
    el.a = 5;
    el.b = {value: 10};
    assert.equal(el.$.boundChild.computedWildcard, 15);
  });

  test('binding with dash', function() {
    el.objectWithDash = {
      'binding-with-dash': 'yes'
    };
    assert.equal(el.$.boundWithDash.textContent, 'yes');
  });

  test('class attribute without template scope not erased', function() {
    var el = document.querySelector('.class1');
    assert.notEqual(el, null, 'class without template scope is undefined');
    var el2 = document.querySelector('.class2');
    assert.notEqual(el2, null, 'class without template scope is undefined');
  });


  test('property effect for native property (title)', function() {
    // NOTE: This will FAIL on browser on which `title` is an own property so
    // just let this pass.
    if (document.createElement('div').hasOwnProperty('title')) {
      this.skip();
    }
    assert.isTrue(el.titleChanged.notCalled);
    el.title = 'a title';
    assert.isTrue(el.titleChanged.calledOnce);
    assert.equal(el.titleChanged.firstCall.args[0], 'a title');
  });

  test('property effect added at instance time', function() {
    el.earlyBoundObserver = sinon.spy();
    el.lateBoundObserver = sinon.spy();
    el.earlyBound = 'early';
    el._createPropertyObserver('earlyBound', 'earlyBoundObserver');
    el._createPropertyObserver('lateBound', 'lateBoundObserver');
    el._flushProperties();
    el.lateBound = 'late';
    assert.equal(el.earlyBound, 'early');
    assert.equal(el.earlyBoundObserver.callCount, 1);
    assert.equal(el.earlyBoundObserver.firstCall.args[0], 'early');
    assert.equal(el.lateBound, 'late');
    assert.equal(el.lateBoundObserver.callCount, 1);
    assert.equal(el.lateBoundObserver.firstCall.args[0], 'late');
  });

  test('does not parse bindings inside <script>', function() {
    assert.include(el.$.scriptWithBinding.textContent, "{{binding}}");
  });

  test('does not parse bindings inside <style>', function() {
    var style = el.shadowRoot.querySelector('style');
    // native shadow dom
    if (style) {
      assert.include(style.textContent, "[[binding]]");
    // shady dom
    } else {
      assert.include(document.querySelector('[scope="x-basic"]').textContent, "[[binding]]");
    }
  });

  test('only calls dynamic functions once', function() {
    el.dynamicFn = function() {
      assert.isFalse(this._dynamicFnCalled);
      this._dynamicFnCalled = true;
    };
  });

  suite('observer inheritance', function() {
    setup(function() {
      el = document.createElement('sub-observer-element');
      document.body.appendChild(el);
    });

    test('does not invoke observer twice', function() {
      assert.equal(el.__observerCalled, 1);
    });
  });

  suite('automated attribute capitalization detection', function() {
    let el;

    setup(function() {
      el = document.createElement('svg-element');
      document.body.appendChild(el);
    });

    teardown(function() {
      document.body.removeChild(el);
    });

    test('can handle capitalized HTML attribute', function() {
      assert.equal(el.$.svg.getAttribute('viewBox'), el.value);
    });
  });

  suite('can work with strict binding parser', function() {
    setup(function() {
      document.body.removeChild(el);
      el = document.createElement('x-basic-strict-binding-parser');
      document.body.appendChild(el);
    });

    test('binding with slash', function() {
      el.objectWithSlash = {
        'binding/with/slash': 'yes'
      };
      assert.equal(el.$.boundWithSlash.textContent, 'yes');
    });

    test('json should not be a binding', function() {
      assert.equal(el.$.jsonContent.textContent, '[["Jan", 31],["Feb", 28],["Mar", 31]]');
    });

    test('binding with non-English unicode', function() {
      el.objectWithNonEnglishUnicode = {
        商品名: 'yes'
      };
      assert.equal(el.$.nonEnglishUnicode.textContent, 'yes');
    });

    test('binding with booleans', function() {
      el.otherValue = 10;
      assert.equal(el.$.booleanTrue.textContent, 'foo(field, true): 10');
      assert.equal(el.$.booleanFalse.textContent, 'foo(field, false): 20');
    });

    suite('equivalent behavior as regex', function() {
      // Loop over the suite "single-element binding effects" (parent of the parent of this suite)
      // And make sure that the tests there also pass on the binding-parser
      //
      // t.title is the name of the test and t.fn contains the test body
      this.parent.parent.tests.forEach(t => {
        test(t.title, t.fn);
      });
    });
  });
});

suite('computed bindings with dynamic functions', function() {

  var el;

  setup(function() {
    sinon.spy(console, 'warn');
  });

  teardown(function() {
    document.body.removeChild(el);
    console.warn.restore();
  });

  test('annotated computation with dynamic function', function() {
    el = document.createElement('x-bind-computed-property');
    document.body.appendChild(el);

    assert.equal(el.$.check.textContent, 'translated: Hello World.');

    el.translator = function(message) {
      return 'changed: ' + message;
    };

    assert.equal(el.$.check.textContent, 'changed: Hello World.');
    assert.equal(console.warn.callCount, 0);
  });

  test('annotated computation / late resolved dynamic function', function() {
    el = document.createElement('x-bind-computed-property-late-translator');
    document.body.appendChild(el);

    assert.equal(el.$.check.textContent.trim(), '');

    el.translator = function(message) {
      return 'translated: ' + message;
    };

    assert.equal(el.$.check.textContent, 'translated: Hello');
    assert.equal(console.warn.callCount, 0);
  });

  test('method observer with dynamic function', function() {
    Polymer({
      is: 'x-method-observer-with-dynamic-function',
      properties: {
        translateMessage: {
          type: Function
        },
        message: {
          type: String,
          value: 'Hello'
        }
      },

      observers: ['translateMessage(message)']

    });

    el = document.createElement('x-method-observer-with-dynamic-function');
    document.body.appendChild(el);

    el.translateMessage = sinon.spy();
    assert.equal(el.translateMessage.callCount, 1);
    assert.equal(console.warn.callCount, 0);
  });

  test('observer with dynamic function', function() {
    Polymer({
      is: 'x-observer-with-dynamic-function',
      properties: {
        messageChanged: {
          type: Function
        },
        message: {
          type: String,
          value: 'Hello',
          observer: 'messageChanged'
        }
      }

    });

    el = document.createElement('x-observer-with-dynamic-function');
    document.body.appendChild(el);

    el.messageChanged = sinon.spy();
    assert.equal(el.messageChanged.callCount, 1);
    assert.equal(console.warn.callCount, 0);
  });

  test('computed property with dynamic function', function() {
    Polymer({
      is: 'x-computed-property-with-dynamic-function',
      properties: {
        computedValue: {
          computed: "translateMessage('Hello')"
        },
        translateMessage: {
          type: Function
        }
      }
    });

    el = document.createElement('x-computed-property-with-dynamic-function');
    document.body.appendChild(el);

    assert.equal(el.computedValue, undefined);

    var called = 0;
    el.translateMessage = function(message) {
      called += 1;
      return 'translated: ' + message;
    };

    assert.equal(called, 1);
    assert.equal(el.computedValue, 'translated: Hello');
    assert.equal(console.warn.callCount, 0);
  });

  test('ensure annotator can pass dynamic fn to parent props', function(done) {
    el = document.createElement('x-child-template-with-dynamic-fn');
    document.body.appendChild(el);

    setTimeout(function() {
      var check = el.root.querySelector('p');
      assert.equal(check.textContent, 'text');
      assert.equal(console.warn.callCount, 0);
      done();
    });

  });

});

suite('2-way binding effects between elements', function() {

  var el;

  setup(function() {
    el = document.createElement('x-compose');
    document.body.appendChild(el);
  });

  teardown(function() {
    document.body.removeChild(el);
  });

  test('binding to non-notifying property', function() {
    el.boundvalue = 42;
    assert.equal(el.$.basic1.value, 42, 'binding to child not updated');
    el.$.basic1.value = 43;
    assert.equal(el.boundvalue, 42, 'binding to non-notifying property updated and should not have been');
  });

  test('observer for property bound to non-notifying property', function() {
    el.$.basic1.value = 44;
    assert.equal(el.observerCounts.boundvalueChanged, 0, 'observer for property bound to non-notifying property called and should not have been');
  });

  test('binding to non-notifying computed property', function() {
    el.boundcomputedvalue = 42;
    el.$.basic1.value = 43;
    assert.equal(el.boundcomputedvalue, 42, 'binding to non-notifying computed property updated and should not have been');
  });

  test('observer for property bound to non-notifying computed property', function() {
    el.$.basic1.value = 44;
    assert.equal(el.observerCounts.boundcomputedvalueChanged, 0, 'observer for property bound to non-notifying computed property called and should not have been');
  });

  test('computed value readOnly from downward binding', function() {
    el.$.basic3.value = 10;
    assert.equal(el.$.basic3.computedvalue, 11);
    // should have no effect
    el.value = 99;
    assert.equal(el.$.basic3.computedvalue, 11);
  });

  test('computed value readOnly from upward notification', function() {
    assert.equal(el.computedValue, 30);
    // should have no effect
    el.$.basic3.notifyingvalue = 10;
    assert.equal(el.computedValue, 30);
  });

  test('binding to notifying property', function() {
    el.boundnotifyingvalue = 42;
    assert.equal(el.$.basic1.notifyingvalue, 42, 'binding to child not updated');
    assert.equal(el.$.basic1.camelNotifyingValue, 42, 'camel-case binding to child not updated');
    el.$.basic1.notifyingvalue = 43;
    assert.equal(el.boundnotifyingvalue, 43, 'binding to notifying property not updated');
    el.$.basic1.camelNotifyingValue = -43;
    assert.equal(el.boundnotifyingvalue, -43, 'camel-case binding to notifying property not updated');
  });

  test('binding to notifying property with default', function() {
    assert.equal(el.boundnotifyingvalueWithDefault, 99);
  });

  test('observer for property bound to notifying property', function() {
    el.$.basic1.notifyingvalue = 45;
    assert.equal(el.observerCounts.boundnotifyingvalueChanged, 1, 'observer for property bound to notifying property not called');
  });

  test('binding to notifying computed property', function() {
    el.$.basic1.notifyingvalue = 43;
    assert.equal(el.boundcomputednotifyingvalue, 45, 'binding to notifying computed property not updated');
  });

  test('observer for property bound to notifying computed property', function() {
    el.$.basic1.notifyingvalue = 45;
    assert.equal(el.observerCounts.boundcomputednotifyingvalueChanged, 1, 'observer for property bound to non-notifying computed property not called');
  });

  test('no change for binding into read-only property', function() {
    el.$.basic1._setReadonlyvalue(45);
    el.$.basic1.clearObserverCounts();
    el.boundreadonlyvalue = 46;
    assert.equal(el.$.basic1.observerCounts.readonlyvalueChanged, 0, 'observer for read-only property should not be called from change to bound value');
    assert.equal(el.$.basic1.readonlyvalue, 45, 'read-only property should not change from change to bound value');
  });

  test('change for binding out of read-only property', function() {
    el.$.basic1._setReadonlyvalue(46);
    assert.equal(el.observerCounts.boundreadonlyvalueChanged, 1, 'observer for property bound to read-only property should be called from change to bound value');
    assert.equal(el.boundreadonlyvalue, 46, 'property bound to read-only property should change from change to bound value');
  });

  test('negated binding update negates value for parent', function() {
    assert.equal(el.negatedValue, false);
    assert.equal(el.$.basic4.notifyingvalue, true);
    el.$.basic4.notifyingvalue = false;
    assert.equal(el.negatedValue, true);
  });

  test('custom xxx-changed event notifies correctly', function() {
    assert.equal(el.boundCustomNotifyingValue, undefined);
    assert.equal(el.observerCounts.boundCustomNotifyingValueChanged, 0);
    el.$.basic1.fireCustomNotifyingEvent();
    assert.equal(el.boundCustomNotifyingValue, 'changed!');
    assert.equal(el.observerCounts.boundCustomNotifyingValueChanged, 1);
  });

});

suite('handling notifying evevnts', function() {

  test('handle notification event and set property with observer when connected', function() {
    var el = document.createElement('x-handle-notify-event');
    document.body.appendChild(el);
    assert.ok(el.shadowRoot.querySelector('#before'), 'element not found before default notifying element');
    assert.ok(el.shadowRoot.querySelector('#later'), 'element not found after default notifying element');
    assert.isTrue(el.readySpy.calledOnce, 'ready called more than once');
    assert.isTrue(el.handleNotify.calledOnce, 'listener called more than once');
    assert.isTrue(el.propChanged.calledOnce, 'observer called more than once');
    assert.isTrue(el.handleNotify.calledBefore(el.propChanged), 'observer called before event');
    assert.isTrue(el.afterSettingProp.calledBefore(el.propChanged), 'accessor side effect processed during notifying event (before clients ready)');
    assert.isTrue(el.propChanged.calledBefore(el.readySpy), 'observer called before ready');
    document.body.removeChild(el);
  });

  test('handle notification event and set property with observer when *not* connected and _enableProperties called', function() {
    var el = document.createElement('x-handle-notify-event');
    el._enableProperties();
    assert.ok(el.shadowRoot.querySelector('#before'), 'element not found before default notifying element');
    assert.ok(el.shadowRoot.querySelector('#later'), 'element not found after default notifying element');
    assert.isTrue(el.readySpy.calledOnce, 'ready called more than once');
    assert.isTrue(el.handleNotify.calledOnce, 'listener called more than once');
    assert.isTrue(el.propChanged.calledOnce, 'observer called more than once');
    assert.isTrue(el.handleNotify.calledBefore(el.propChanged), 'observer called before event');
    assert.isTrue(el.afterSettingProp.calledBefore(el.propChanged), 'accessor side effect processed during notifying event (before clients ready)');
    assert.isTrue(el.propChanged.calledBefore(el.readySpy), 'observer called before ready');
  });

});

suite('1-way binding effects between elements', function() {

  var el;

  setup(function() {
    el = document.createElement('x-compose');
    document.body.appendChild(el);
  });

  teardown(function() {
    document.body.removeChild(el);
  });

  test('one-way binding to non-notifying property', function() {
    el.boundvalue = 42;
    assert.equal(el.$.basic1.value, 42, 'binding to child not updated');
    el.$.basic2.value = 43;
    assert.equal(el.boundvalue, 42, 'binding to non-notifying property updated and should not have been');
  });

  test('observer for property one-way-bound to non-notifying property', function() {
    el.$.basic2.value = 44;
    assert.equal(el.observerCounts.boundvalueChanged, 0, 'observer for property one-way-bound to non-notifying property called and should not have been');
  });

  test('one-way binding to non-notifying computed property', function() {
    el.boundcomputedvalue = 42;
    el.$.basic2.value = 43;
    assert.equal(el.boundcomputedvalue, 42, 'binding to non-notifying computed property updated and should not have been');
  });

  test('observer for property one-way-bound to non-notifying computed property', function() {
    el.$.basic2.value = 44;
    assert.equal(el.observerCounts.boundcomputedvalueChanged, 0, 'observer for property bound to non-notifying computed property called and should not have been');
  });

  test('one-way binding to notifying property', function() {
    el.boundnotifyingvalue = 42;
    assert.equal(el.$.basic2.notifyingvalue, 42, 'binding to child not updated');
    el.$.basic2.notifyingvalue = 43;
    assert.equal(el.boundnotifyingvalue, 42, 'binding to notifying property updated and should not have been');
  });

  test('observer for property one-way-bound to notifying property', function() {
    el.$.basic2.notifyingvalue = 45;
    assert.equal(el.observerCounts.boundnotifyingvalueChanged, 0, 'observer for property bound to notifying property called and should not have been');
  });

  test('one-way binding to notifying computed property', function() {
    el.boundcomputednotifyingvalue = 42;
    el.$.basic2.notifyingvalue = 43;
    assert.equal(el.boundcomputednotifyingvalue, 42, 'binding to notifying computed property updated and should not have been');
  });

  test('observer for property one-way-bound to notifying computed property', function() {
    el.$.basic2.notifyingvalue = 45;
    assert.equal(el.observerCounts.boundcomputednotifyingvalueChanged, 0, 'observer for property bound to non-notifying computed property called and should not have been');
  });

});

suite('reflection to attribute', function() {

  var el;

  setup(function() {
    el = document.createElement('x-reflect');
    document.body.appendChild(el);
  });

  teardown(function() {
    document.body.removeChild(el);
  });

  test('reflect object', function() {
    var obj = {foo: 'bar', array: [1, '2', {3:3}]};
    el.reflectedobject = obj;
    assert.equal(el.getAttribute('reflectedobject'), '{"foo":"bar","array":[1,"2",{"3":3}]}');
    // Ensure object wasn't re-deserialized
    assert.equal(el.reflectedobject, obj);
    el.reflectedobject = null;
    assert(!el.hasAttribute('reflectedobject'));
  });

  test('reflect array', function() {
    var arr = [1, '2', {3:3}, {'four': 'four'}];
    el.reflectedarray = arr;
    assert.equal(el.getAttribute('reflectedarray'), '[1,"2",{"3":3},{"four":"four"}]');
    // Ensure array wasn't re-deserialized
    assert.equal(el.reflectedarray, arr);
    el.reflectedarray = null;
    assert(!el.hasAttribute('reflectedarray'));
  });

  test('reflect string', function() {
    var str = '"polymer is grrrrreat, ain\'t it?"';
    el.reflectedstring = str;
    assert.equal(el.getAttribute('reflectedstring'), str);
    assert.equal(el.reflectedstring, str);
    el.reflectedstring = '';
    assert.equal(el.getAttribute('reflectedstring'), '');
    assert.equal(el.reflectedstring, '');
    el.reflectedstring = null;
    assert(!el.hasAttribute('reflectedstring'));
    assert.equal(el.reflectedstring, null);
  });

  test('reflect number', function() {
    el.reflectedNumber = 765;
    assert.equal(el.getAttribute('reflected-number'), '765');
    assert.equal(el.reflectedNumber, 765);
    el.reflectedNumber = 765.4321;
    assert.equal(el.getAttribute('reflected-number'), '765.4321');
    assert.equal(el.reflectedNumber, 765.4321);
    el.reflectedNumber = null;
    assert(!el.hasAttribute('reflected-number'));
    assert.equal(el.reflectedNumber, null);
  });

  test('reflect boolean', function() {
    el.reflectedboolean = true;
    assert(el.hasAttribute('reflectedboolean'));
    assert.equal(el.getAttribute('reflectedboolean'), '');
    assert.equal(el.reflectedboolean, true);
    el.reflectedboolean = false;
    assert(!el.hasAttribute('reflectedboolean'));
    assert.equal(el.reflectedboolean, false);
    el.reflectedboolean = true;
    el.reflectedboolean = null;
    assert(!el.hasAttribute('reflectedboolean'));
    assert.equal(el.reflectedboolean, null);
  });

  test('reflect date', function() {
    var date = new Date('Fri Jan 23 2015 17:40:29 GMT-0800 (PST)');
    el.reflecteddate = date;
    assert(el.hasAttribute('reflecteddate'));
    assert.equal(Date.parse(el.getAttribute('reflecteddate')),
      el.reflecteddate.getTime());
    assert.equal(el.reflecteddate, date);
    el.reflecteddate = null;
    assert(!el.hasAttribute('reflecteddate'));
    assert.equal(el.reflecteddate, null);
  });

  test('reflect wrong type', function() {
    el.reflectedstring = true;
    assert(el.hasAttribute('reflectedstring'));
    assert.equal(el.getAttribute('reflectedstring'), '');
    // Ensure value wasn't re-deserialized
    assert.strictEqual(el.reflectedstring, true);
  });

});

suite('binding to attribute', function() {

  var el;

  setup(function() {
    el = document.createElement('x-basic');
    document.body.appendChild(el);
  });

  teardown(function() {
    document.body.removeChild(el);
  });

  test('bind object to attribute', function() {
    el.attrvalue = {foo: 'bar', array: [1, '2', {3:3}]};
    assert.equal(el.$.boundChild.getAttribute('attrvalue'),
      '{"foo":"bar","array":[1,"2",{"3":3}]}');
    el.attrvalue = null;
    assert(!el.$.boundChild.hasAttribute('attrvalue'));
  });

  test('bind array to attribute', function() {
    el.attrvalue = [1, '2', {3:3}, {'four': 'four'}];
    assert.equal(el.$.boundChild.getAttribute('attrvalue'),
      '[1,"2",{"3":3},{"four":"four"}]');
    el.attrvalue = null;
    assert(!el.$.boundChild.hasAttribute('attrvalue'));
  });

  test('bind string to attribute', function() {
    el.attrvalue = '"polymer is grrrrreat, ain\'t it?"';
    assert.equal(el.$.boundChild.getAttribute('attrvalue'),
      '"polymer is grrrrreat, ain\'t it?"');
    el.attrvalue = '';
    assert.equal(el.$.boundChild.getAttribute('attrvalue'), '');
    el.attrvalue = null;
    assert(!el.$.boundChild.hasAttribute('attrvalue'));
  });

  test('bind number to attribute', function() {
    el.attrvalue = 765;
    assert.equal(el.$.boundChild.getAttribute('attrvalue'), '765');
    el.attrvalue = 765.4321;
    assert.equal(el.$.boundChild.getAttribute('attrvalue'), '765.4321');
    el.attrvalue = null;
    assert(!el.$.boundChild.hasAttribute('attrvalue'));
  });

  test('bind boolean to attribute', function() {
    el.attrvalue = true;
    assert(el.$.boundChild.hasAttribute('attrvalue'));
    assert.equal(el.$.boundChild.getAttribute('attrvalue'), '');
    el.attrvalue = false;
    assert(!el.$.boundChild.hasAttribute('attrvalue'));
    el.attrvalue = true;
    el.attrvalue = null;
    assert(!el.$.boundChild.hasAttribute('attrvalue'));
  });

  test('bind date to attribute', function() {
    el.attrvalue = new Date('Fri Jan 23 2015 17:40:29 GMT-0800 (PST)');
    assert(el.$.boundChild.hasAttribute('attrvalue'));
    assert.equal(Date.parse(el.$.boundChild.getAttribute('attrvalue')),
      el.attrvalue.getTime());
    el.attrvalue = null;
    assert(!el.$.boundChild.hasAttribute('attrvalue'));
  });

  test('bind to value attribute on input should not fail', function() {
    var el = document.createElement('x-input-value');
    document.body.appendChild(el);
    el.inputValue = "the value";
    assert.equal(el.$.input.value, "the value", "The value of the input is not propagated");
    assert.equal(el.$.input.value$, undefined, "value$ should be removed from input");
    document.body.removeChild(el);
  });

});

suite('avoid non-bubbling event gotchas', function() {

  var el;
  var container;

  setup(function() {
    container = document.createElement('div');
    document.body.appendChild(container);
    container.innerHTML = '<x-notifies3></x-notifies3>';
    el = container.firstChild;
  });

  teardown(function() {
    document.body.removeChild(container);
  });

  test('avoid non-bubbling event gotchas', function() {
    el.$.notifies2.$.notifies1.notifies = 'runtimeValue';
    assert.equal(el.$.notifies2.$.notifies1.notifies, 'runtimeValue');
    assert.equal(el.$.notifies2.shouldChange, 'runtimeValue');
    assert.notEqual(el.shouldNotChange, 'runtimeValue');
  });

  test('avoid non-bubbling event gotchas at ready time', function() {
    assert.equal(el.$.notifies2.$.notifies1.notifies, 'readyValue');
    assert.equal(el.$.notifies2.shouldChange, 'readyValue');
    assert.notEqual(el.shouldNotChange, 'readyValue');
  });

});

suite('warnings', function() {

  var el;
  var warn; //eslint-disable-line no-unused-vars

  setup(function() {
    sinon.spy(console, 'warn');
    el = document.createElement('x-basic');
    document.body.appendChild(el);
  });

  teardown(function() {
    console.warn.restore();
    document.body.removeChild(el);
  });

  test('undefined observer', function() {
    el.noObserver = 42;
    assert.isTrue(console.warn.calledOnce, 'no warning for undefined observer');
  });

  test('undefined complex observer', function() {
    el.noComplexObserver = {};
    assert.isTrue(console.warn.calledOnce, 'no warning for undefined complex observer');
  });

  test('undefined computed function', function() {
    el.noComputed = 99;
    assert.isTrue(console.warn.calledOnce, 'no warning for undefined computed function');
  });

  test('undefined inline computed function', function() {
    el.noInlineComputed = 99;
    assert.isTrue(console.warn.calledOnce, 'no warning for undefined computed function');
  });

  test('binding to a bad attribute warns', function() {
    document.createElement('x-bind-bad-attribute-name');
    assert.isTrue(console.warn.calledOnce, 'no warning for setting a bad attribute');
  });

});

suite('binding corner cases', function() {

  // IE can create adjacent text nodes that split bindings; this test
  // ensures the code that addresses this is functional
  test('text binding after entity', function() {
    var el = document.createElement('x-entity-and-binding');
    document.body.appendChild(el);
    assert.equal(el.$.binding.textContent, 'binding');
    document.body.removeChild(el);
  });

  test('bind to isAttached', function() {
    var el = document.createElement('x-bind-is-attached');
    sinon.spy(el, '_isAttachedChanged');
    document.body.appendChild(el);
    assert.equal(el.$.check.textContent, 'true');
    assert.isTrue(el._isAttachedChanged.calledOnce);
    document.body.removeChild(el);
  });

});

suite('binding interop', function() {

  test('do not set same value to a non-Polymer element', function() {
    var el = document.createElement('x-interop');
    document.body.appendChild(el);
    assert.equal(el.$.raw.value, 10);
    assert.equal(el.$.raw.valueChanged.callCount, 1);
    assert.equal(el.$.raw.valueChanged.firstCall.args[0], 10);
    // Should set value
    el.value++;
    assert.equal(el.$.raw.value, 11);
    assert.equal(el.$.raw.valueChanged.callCount, 2);
    assert.equal(el.$.raw.valueChanged.secondCall.args[0], 11);
    // Notifies up to host, should not be re-set down to element
    el.$.raw.increment();
    assert.equal(el.$.raw.value, 12);
    assert.equal(el.$.raw.valueChanged.callCount, 2);
    document.body.removeChild(el);
  });

  test('path notification of object bound to textContent of a Polymer element', function() {
    var el = document.createElement('x-interop');
    document.body.appendChild(el);
    // Initial state
    assert.equal(el.$.polymer.textContent, '1,2,3');
    // Push
    el.push('array', 4);
    assert.equal(el.$.polymer.textContent, '1,2,3,4');
    document.body.removeChild(el);
  });

  test('path notification of array bound to setter of a Polymer element', function() {
    var el = document.createElement('x-interop');
    document.body.appendChild(el);
    // Initial state
    assert.equal(el.$.polymer.array, el.array);
    assert.equal(el.$.polymer.arrayChanged.callCount, 1);
    assert.equal(el.$.polymer.arrayChanged.firstCall.args[0], el.array);
    // Push
    el.push('array', 4);
    assert.equal(el.$.polymer.array, el.array);
    // Gets called twice, once for splice info, once for length :(
    assert.equal(el.$.polymer.arrayChanged.callCount, 3);
    assert.equal(el.$.polymer.arrayChanged.secondCall.args[0], el.array);
    assert.equal(el.$.polymer.arrayChanged.thirdCall.args[0], el.array);
    document.body.removeChild(el);
  });

  test('path notification of array in compound binding to property of Polymer element', function() {
    var el = document.createElement('x-interop');
    document.body.appendChild(el);
    // Initial state
    assert.equal(el.$.polymer.compound, '**1,2,3**');
    assert.equal(el.$.polymer.compoundChanged.callCount, 1);
    assert.equal(el.$.polymer.compoundChanged.firstCall.args[0], '**1,2,3**');
    // Push
    el.push('array', 4);
    assert.equal(el.$.polymer.compound, '**1,2,3,4**');
    assert.equal(el.$.polymer.compoundChanged.callCount, 2);
    assert.equal(el.$.polymer.compoundChanged.secondCall.args[0], '**1,2,3,4**');
    document.body.removeChild(el);
  });

  test('path notification of array bound to setter of a non-Polymer element', function() {
    var el = document.createElement('x-interop');
    document.body.appendChild(el);
    // Initial state
    assert.equal(el.$.raw.textContent, '1,2,3');
    assert.equal(el.$.raw.arrayChanged.callCount, 1);
    assert.equal(el.$.raw.arrayChanged.firstCall.args[0], el.array);
    // Push
    el.push('array', 4);
    assert.equal(el.$.raw.textContent, '1,2,3,4');
    // Array change notifies once for splice info, once for length, and
    // since the property is a Array, it passes the dirty check and will go
    // through twice; this test also happens to not have a getter, so that
    // would cause the dirty check to pass even if it wasn't an Object
    assert.equal(el.$.raw.arrayChanged.callCount, 3);
    assert.equal(el.$.raw.arrayChanged.secondCall.args[0], el.array);
    assert.equal(el.$.raw.arrayChanged.thirdCall.args[0], el.array);
    document.body.removeChild(el);
  });

  test('path notification of array in compound binding to property of non-Polymer element', function() {
    var el = document.createElement('x-interop');
    document.body.appendChild(el);
    // Initial state
    assert.equal(el.$.raw.compound, '**1,2,3**');
    // Raw elements with compound bindings will first see the
    // literals, then the properties, hence 2 sets initially
    assert.equal(el.$.raw.compoundChanged.callCount, 2);
    assert.equal(el.$.raw.compoundChanged.firstCall.args[0], '****');
    assert.equal(el.$.raw.compoundChanged.secondCall.args[0], '**1,2,3**');
    // Push
    el.push('array', 4);
    assert.equal(el.$.raw.compound, '**1,2,3,4**');
    assert.equal(el.$.raw.compoundChanged.callCount, 3);
    assert.equal(el.$.raw.compoundChanged.thirdCall.args[0], '**1,2,3,4**');
    document.body.removeChild(el);
  });

});

suite('compound binding / string interpolation', function() {

  var el;

  setup(function() {
    el = document.createElement('x-basic');
    document.body.appendChild(el);
  });

  teardown(function() {
    document.body.removeChild(el);
  });

  test('compound adjacent property bindings', function() {
    // Adjacent compound binding with no literal do not override the default
    assert.equal(el.$.boundProps.prop1, 'default');
    assert.isTrue(el.$.boundProps.prop1Changed.calledOnce);
    el.cpnd2 = 'cpnd2';
    assert.equal(el.$.boundProps.prop1, 'cpnd2');
    el.cpnd1 = 'cpnd1';
    el.cpnd3 = {prop: 'cpnd3'};
    assert.equal(el.$.boundProps.prop1, 'cpnd1cpnd2cpnd3');
    el.cpnd4 = 'cpnd4';
    assert.equal(el.$.boundProps.prop1,
      legacyUndefined ?
      'cpnd1cpnd2cpnd3' : 'cpnd1cpnd2cpnd3literalComputedcpnd4');
    el.cpnd5 = 'cpnd5';
    assert.equal(el.$.boundProps.prop1, 'cpnd1cpnd2cpnd3literalComputedcpnd5cpnd4');
  });

  test('compound property bindings with literals', function() {
    assert.equal(el.$.boundProps.prop2, 'literal1  literal2  literal3  literal4');
    assert.isTrue(el.$.boundProps.prop2Changed.calledOnce);
    el.cpnd1 = 'cpnd1';
    el.cpnd2 = 'cpnd2';
    el.cpnd3 = {prop: 'cpnd3'};
    el.cpnd4 = 'cpnd4';
    el.cpnd5 = 'cpnd5';
    assert.equal(el.$.boundProps.prop2, 'literal1 cpnd1 literal2 cpnd2cpnd3 literal3 literalComputedcpnd5cpnd4 literal4');
    el.cpnd1 = null;
    el.cpnd2 = undefined;
    el.cpnd3 = {};
    el.cpnd4 = '';
    el.cpnd5 = '';
    assert.equal(el.$.boundProps.prop2, 'literal1  literal2  literal3 literalComputed literal4');
  });

  test('compound adjacent attribute bindings', function() {
    // Adjacent compound binding with no literal do not override the default
    assert.equal(el.$.boundChild.getAttribute('compoundAttr1'), null);
    el.cpnd2 = 'cpnd2';
    assert.equal(el.$.boundChild.getAttribute('compoundAttr1'), 'cpnd2');
    el.cpnd1 = 'cpnd1';
    el.cpnd3 = {prop: 'cpnd3'};
    assert.equal(el.$.boundChild.getAttribute('compoundAttr1'), 'cpnd1cpnd2cpnd3');
    el.cpnd4 = 'cpnd4';
    assert.equal(el.$.boundChild.getAttribute('compoundAttr1'),
      legacyUndefined ?
      'cpnd1cpnd2cpnd3' : 'cpnd1cpnd2cpnd3literalComputedcpnd4');
    el.cpnd5 = 'cpnd5';
    assert.equal(el.$.boundChild.getAttribute('compoundAttr1'), 'cpnd1cpnd2cpnd3literalComputedcpnd5cpnd4');
  });

  test('compound property attribute with literals', function() {
    assert.equal(el.$.boundChild.getAttribute('compoundAttr2'), 'literal1  literal2  literal3  literal4');
    el.cpnd1 = 'cpnd1';
    el.cpnd2 = 'cpnd2';
    el.cpnd3 = {prop: 'cpnd3'};
    el.cpnd4 = 'cpnd4';
    el.cpnd5 = 'cpnd5';
    assert.equal(el.$.boundChild.getAttribute('compoundAttr2'), 'literal1 cpnd1 literal2 cpnd2cpnd3 literal3 literalComputedcpnd5cpnd4 literal4');
    el.cpnd1 = null;
    el.cpnd2 = undefined;
    el.cpnd3 = {};
    el.cpnd4 = '';
    el.cpnd5 = '';
    assert.equal(el.$.boundChild.getAttribute('compoundAttr2'), 'literal1  literal2  literal3 literalComputed literal4');
  });

  test('compound property attribute with {} and [] in text', function() {
    el.cpnd1 = 'cpnd1';
    assert.equal(el.$.boundChild.getAttribute('compoundAttr3'), '[yes/no]: cpnd1, Hello {0} username world');
  });

  test('compound adjacent textNode bindings', function() {
    // The single space is due to the gambit to prevent empty text nodes
    // from being omitted by IE during importNode from the template; it will
    // only be there when in the virgin state after cloning the template
    assert.equal(el.$.compound1.textContent, ' ');
    el.cpnd2 = 'cpnd2';
    assert.equal(el.$.compound1.textContent, 'cpnd2');
    el.cpnd1 = 'cpnd1';
    el.cpnd3 = {prop: 'cpnd3'};
    assert.equal(el.$.compound1.textContent, 'cpnd1cpnd2cpnd3');
    el.cpnd4 = 'cpnd4';
    assert.equal(el.$.compound1.textContent,
      legacyUndefined ?
      'cpnd1cpnd2cpnd3' : 'cpnd1cpnd2cpnd3literalComputedcpnd4');
    el.cpnd5 = 'cpnd5';
    assert.equal(el.$.compound1.textContent, 'cpnd1cpnd2cpnd3literalComputedcpnd5cpnd4');
    // Once the binding evaluates back to '', it will in fact be ''
    el.computeCompound = function() { return ''; };
    el.cpnd1 = null;
    el.cpnd2 = '';
    el.cpnd3 = {prop: null};
    el.cpnd4 = null;
    el.cpnd5 = '';
    assert.equal(el.$.compound1.textContent, '');
  });

  test('compound textNode bindings with literals', function() {
    assert.equal(el.$.compound2.textContent.trim(), 'literal1  literal2  literal3  literal4');
    el.cpnd1 = 'cpnd1';
    el.cpnd2 = 'cpnd2';
    el.cpnd3 = {prop: 'cpnd3'};
    el.cpnd4 = 'cpnd4';
    el.cpnd5 = 'cpnd5';
    assert.equal(el.$.compound2.textContent.trim(), 'literal1 cpnd1 literal2 cpnd2cpnd3 literal3 literalComputedcpnd5cpnd4 literal4');
    el.cpnd1 = null;
    el.cpnd2 = undefined;
    el.cpnd3 = {};
    el.cpnd4 = '';
    el.cpnd5 = '';
    assert.equal(el.$.compound2.textContent.trim(), 'literal1  literal2  literal3 literalComputed literal4');
  });

  test('malformed bindings ignored', function() {
    el.bool = true;
    assert.isTrue(el.$.boundChild.textContent.indexOf('really.long.identifier.in.malformed.binding.should.be.ignored') >= 0, true);
    assert.isTrue(el.$.boundChild.textContent.indexOf('really.long.literal.in.malformed.binding.should.be.ignored') >= 0, true);
    assert.isTrue(el.$.boundChild.textContent.indexOf('3foo') >= 0, true);
  });

});

suite('order of effects', function() {

  var el;

  setup(function() {
    let gp = document.createElement('x-order-of-effects-grand-parent');
    document.body.appendChild(gp);
    el = gp.$.child;
  });

  test('effects are sorted', function() {
    assert.equal(el.invocations.length, 0);
    el.base = 'changed';

    var expected = [
      'compute',
      'propagate',  // as observed by child; note: order of binding vs.
      'propagate',  // computed binding is not guaranteed
      'reflect',
      'observe',    // note: order of single-property observer vs.
      'observe',    // multi-property observer is not guaranteed
      'notify'      // as observed by grand-parent
    ];

    assert.deepEqual(el.invocations, expected);
  });
});

suite('misc', function() {

  test('effects forward propagate value', function() {
    var el = document.createElement('x-propagate');
    document.body.appendChild(el);
    assert.equal(el.value, 1, 'observers did not receive latest value for property');
    document.body.removeChild(el);
  });

  test('custom _template on prototype', function() {
    var el = document.createElement('x-template-proto');
    document.body.appendChild(el);
    if (window.ShadyDOM) {
      ShadyDOM.flush();
    }
    assert.equal(el.$.div.textContent, 'yes');
    assert.equal(el.clicked.callCount, 0);
    el.$.div.click();
    assert.equal(el.clicked.callCount, 1);
    document.body.removeChild(el);
  });

  test('custom _template on behavior', function() {
    var el = document.createElement('x-template-behavior');
    document.body.appendChild(el);
    if (window.ShadyDOM) {
      ShadyDOM.flush();
    }
    assert.equal(el.$.div.textContent, 'yes');
    assert.equal(el.clicked.callCount, 0);
    el.$.div.click();
    assert.equal(el.clicked.callCount, 1);
    document.body.removeChild(el);
  });

});

suite('DOM sanitization', function() {

  setup(function() {
    setSanitizeDOMValue(sinon.spy(function(value) {
      return (value && value.toString().indexOf('javascript') >= 0) ? 'notallowed!' : value;
    }));
  });

  teardown(function() {
    setSanitizeDOMValue(null);
  });

  test('bound property', function() {
    var el = document.createElement('x-basic');
    document.body.appendChild(el);
    el.sanitizeValue = 'ok';
    assert.equal(el.$.boundChild.sanitizeValue, 'ok');
    el.sanitizeValue = 'javascript';
    assert.equal(el.$.boundChild.sanitizeValue, 'notallowed!');
    assert.deepEqual(sanitizeDOMValue.lastCall.args,
      ['javascript', 'sanitizeValue', 'property', el.$.boundChild]);
  });

  test('bound textContent', function() {
    var el = document.createElement('x-basic');
    document.body.appendChild(el);
    el.sanitizeText = 'ok';
    assert.equal(el.$.sanitizeText.textContent, 'ok');
    el.sanitizeText = 'javascript';
    assert.equal(el.$.sanitizeText.textContent, 'notallowed!');
    assert.deepEqual(sanitizeDOMValue.lastCall.args,
      ['javascript', 'textContent', 'text', el.$.sanitizeText.firstChild]);
  });

  test('bound attribute', function() {
    var el = document.createElement('x-basic');
    document.body.appendChild(el);
    el.attrvalue = 'ok';
    assert.equal(el.$.boundChild.getAttribute('attrvalue'), 'ok');
    el.attrvalue = 'javascript';
    assert.equal(el.$.boundChild.getAttribute('attrvalue'), 'notallowed!');
    assert.deepEqual(sanitizeDOMValue.lastCall.args,
      ['javascript', 'attrvalue', 'attribute', el.$.boundChild]);
  });

  test('reflected attribute', function() {
    var el = document.createElement('x-reflect');
    document.body.appendChild(el);
    el.reflectedstring = 'ok';
    assert.equal(el.getAttribute('reflectedstring'), 'ok');
    el.reflectedstring = 'javascript';
    assert.equal(el.getAttribute('reflectedstring'), 'notallowed!');
    assert.deepEqual(sanitizeDOMValue.lastCall.args,
      ['javascript', 'reflectedstring', 'attribute', el]);
  });

  test('getter returns the current value', function() {
    assert(sanitizeDOMValue instanceof Function);
    assert.strictEqual(getSanitizeDOMValue(), sanitizeDOMValue);
    setSanitizeDOMValue(null);
    assert.strictEqual(sanitizeDOMValue, null);
    assert.strictEqual(getSanitizeDOMValue(), null);
  });
});

suite('data (im)mutability', function() {

  test('immutable data propagation', function() {
    let el = document.createElement('x-immutable-a');
    document.body.appendChild(el);
    // Flow initial data
    el.a = {x: 'xb', b: {x: 'xc', c: 'c'}};
    assert.equal(el.a, el.a);
    assert.equal(el.aChanged.callCount, 1);
    assert.equal(el.$.b.b, el.a.b);
    assert.equal(el.$.b.x, 'xb');
    assert.equal(el.$.b.bChanged.callCount, 1);
    assert.equal(el.$.b.xChanged.callCount, 1);
    assert.equal(el.$.b.$.c.c, 'c');
    assert.equal(el.$.b.$.c.x, 'xc');
    assert.equal(el.$.b.$.c.cChanged.callCount, 1);
    assert.equal(el.$.b.$.c.xChanged.callCount, 1);
    // Mutate in place with reset (error, does nothing)
    el.a.b.c = 'wontchange';
    el.a = el.a;
    assert.equal(el.a, el.a);
    assert.equal(el.aChanged.callCount, 1);
    assert.equal(el.$.b.b, el.a.b);
    assert.equal(el.$.b.x, 'xb');
    assert.equal(el.$.b.bChanged.callCount, 1);
    assert.equal(el.$.b.xChanged.callCount, 1);
    assert.equal(el.$.b.$.c.c, 'c');
    assert.equal(el.$.b.$.c.x, 'xc');
    assert.equal(el.$.b.$.c.cChanged.callCount, 1);
    assert.equal(el.$.b.$.c.xChanged.callCount, 1);
    // Mutate via immutable pattern:
    el.a = Object.assign({}, el.a, {b:
      Object.assign({}, el.a.b, {
        c: 'willchange1'
      })
    });
    assert.equal(el.a, el.a);
    assert.equal(el.aChanged.callCount, 2);
    assert.equal(el.$.b.b, el.a.b);
    assert.equal(el.$.b.x, 'xb');
    assert.equal(el.$.b.bChanged.callCount, 2);
    assert.equal(el.$.b.xChanged.callCount, 1);
    assert.equal(el.$.b.$.c.c, 'willchange1');
    assert.equal(el.$.b.$.c.x, 'xc');
    assert.equal(el.$.b.$.c.cChanged.callCount, 2);
    assert.equal(el.$.b.$.c.xChanged.callCount, 1);
    // Mutate via path API
    el.set('a.b.c', 'willchange2');
    assert.equal(el.a, el.a);
    assert.equal(el.aChanged.callCount, 2);
    assert.equal(el.$.b.b, el.a.b);
    assert.equal(el.$.b.x, 'xb');
    assert.equal(el.$.b.bChanged.callCount, 2);
    assert.equal(el.$.b.xChanged.callCount, 1);
    assert.equal(el.$.b.$.c.c, 'willchange2');
    assert.equal(el.$.b.$.c.x, 'xc');
    assert.equal(el.$.b.$.c.cChanged.callCount, 3);
    assert.equal(el.$.b.$.c.xChanged.callCount, 1);
  });

  test('mutable data propagation', function() {
    let el = document.createElement('x-mutable-a');
    document.body.appendChild(el);
    // Flow initial data
    el.a = {x: 'xb', b: {x: 'xc', c: 'c'}};
    assert.equal(el.a, el.a);
    assert.equal(el.aChanged.callCount, 1);
    assert.equal(el.$.b.b, el.a.b);
    assert.equal(el.$.b.x, 'xb');
    assert.equal(el.$.b.bChanged.callCount, 1);
    assert.equal(el.$.b.xChanged.callCount, 1);
    assert.equal(el.$.b.$.c.c, 'c');
    assert.equal(el.$.b.$.c.x, 'xc');
    assert.equal(el.$.b.$.c.cChanged.callCount, 1);
    assert.equal(el.$.b.$.c.xChanged.callCount, 1);
    // Mutate in place with reset
    el.a.b.c = 'willchange1';
    el.a = el.a;
    assert.equal(el.a, el.a);
    assert.equal(el.aChanged.callCount, 2);
    assert.equal(el.$.b.b, el.a.b);
    assert.equal(el.$.b.x, 'xb');
    assert.equal(el.$.b.bChanged.callCount, 2);
    assert.equal(el.$.b.xChanged.callCount, 1);
    assert.equal(el.$.b.$.c.c, 'willchange1');
    assert.equal(el.$.b.$.c.x, 'xc');
    assert.equal(el.$.b.$.c.cChanged.callCount, 2);
    assert.equal(el.$.b.$.c.xChanged.callCount, 1);
    // Mutate via immutable pattern:
    el.a = Object.assign({}, el.a, {b:
      Object.assign({}, el.a.b, {
        c: 'willchange2'
      })
    });
    assert.equal(el.a, el.a);
    assert.equal(el.aChanged.callCount, 3);
    assert.equal(el.$.b.b, el.a.b);
    assert.equal(el.$.b.x, 'xb');
    assert.equal(el.$.b.bChanged.callCount, 3);
    assert.equal(el.$.b.xChanged.callCount, 1);
    assert.equal(el.$.b.$.c.c, 'willchange2');
    assert.equal(el.$.b.$.c.x, 'xc');
    assert.equal(el.$.b.$.c.cChanged.callCount, 3);
    assert.equal(el.$.b.$.c.xChanged.callCount, 1);
    // Mutate via path API
    el.set('a.b.c', 'willchange3');
    assert.equal(el.a, el.a);
    assert.equal(el.aChanged.callCount, 3);
    assert.equal(el.$.b.b, el.a.b);
    assert.equal(el.$.b.x, 'xb');
    assert.equal(el.$.b.bChanged.callCount, 3);
    assert.equal(el.$.b.xChanged.callCount, 1);
    assert.equal(el.$.b.$.c.c, 'willchange3');
    assert.equal(el.$.b.$.c.x, 'xc');
    assert.equal(el.$.b.$.c.cChanged.callCount, 4);
    assert.equal(el.$.b.$.c.xChanged.callCount, 1);
  });

});

suite('runtime effects', function() {

  var el, el2;

  setup(function() {
    el = document.createElement('x-basic');
    document.body.appendChild(el);
  });

  teardown(function() {
    document.body.removeChild(el);
    document.body.removeChild(el2);
  });

  test('add/remove runtime property effect', function() {
    assert.equal(el.observerCounts.valueChanged, 1);
    let info = {};
    let fn = sinon.spy();
    let effect = { info, fn };
    el._addPropertyEffect('value', el.PROPERTY_EFFECT_TYPES.OBSERVE, effect);
    el.value = 'value++';
    assert.equal(el.observerCounts.valueChanged, 2);
    assert.equal(fn.callCount, 1);
    assert.equal(fn.firstCall.args[0], el);
    assert.equal(fn.firstCall.args[1], 'value');
    assert.equal(fn.firstCall.args[2].value, 'value++');
    assert.equal(fn.firstCall.args[4], info);
    el._removePropertyEffect('value', el.PROPERTY_EFFECT_TYPES.OBSERVE, effect);
    el.value = 'value+++';
    assert.equal(fn.callCount, 1);
    // Ensure prototype wasn't affected
    el2 = document.createElement('x-basic');
    document.body.appendChild(el2);
    assert.equal(fn.callCount, 1);
  });

  test('add/remove runtime path effect', function() {
    assert.equal(el.observerCounts.valueChanged, 1);
    let info = {};
    let fn = sinon.spy();
    let trigger = {name: 'value.path', structured: true};
    let effect = { info, fn, trigger };
    el._addPropertyEffect('value', el.PROPERTY_EFFECT_TYPES.OBSERVE, effect);
    el.value = {path: 'value.path'};
    assert.equal(el.observerCounts.valueChanged, 2);
    assert.equal(fn.callCount, 1);
    assert.equal(fn.getCall(0).args[0], el);
    assert.equal(fn.getCall(0).args[1], 'value');
    assert.equal(fn.getCall(0).args[2].value, el.value);
    assert.equal(fn.getCall(0).args[4], info);
    el.set('value.path', 'value.path++');
    assert.equal(el.observerCounts.valueChanged, 2);
    assert.equal(fn.callCount, 2);
    assert.equal(fn.getCall(1).args[0], el);
    assert.equal(fn.getCall(1).args[1], 'value.path');
    assert.equal(fn.getCall(1).args[2]['value.path'], 'value.path++');
    assert.equal(fn.getCall(1).args[4], info);
    el._removePropertyEffect('value', el.PROPERTY_EFFECT_TYPES.OBSERVE, effect);
    el.set('value.path', 'value.path+++');
    assert.equal(fn.callCount, 2);
    // Ensure prototype wasn't affected
    el2 = document.createElement('x-basic');
    document.body.appendChild(el2);
    assert.equal(fn.callCount, 2);
  });

});

suite('a la carte usage of API', function() {

  let BaseClass;

  suiteSetup(function() {
    BaseClass = class extends PropertyEffects(HTMLElement) {
      connectedCallback() {
        this.pcSpy = sinon.spy();
        this._enableProperties();
      }
      _propertiesChanged(props, changedProps, oldProps) {
        this.pcSpy(props, changedProps, oldProps);
        super._propertiesChanged(props, changedProps, oldProps);
      }
    };
  });

  test('generic property effect', function() {
    class TestClass extends BaseClass {
      constructor() {
        super();
        this.observer = sinon.spy();
      }
    }
    let observer = sinon.spy();
    TestClass.addPropertyEffect('prop', TestClass.prototype.PROPERTY_EFFECT_TYPES.OBSERVE, {
      fn: observer
    });
    customElements.define('pe-pe', TestClass);
    let el = document.createElement('pe-pe');
    document.body.appendChild(el);
    assert.equal(observer.callCount, 0);
    assert.equal(el.pcSpy.callCount, 0);
    el.prop = true;
    assert.equal(observer.callCount, 1);
    assert.equal(el.pcSpy.callCount, 1);
    el.prop = 'hithere';
    assert.equal(observer.callCount, 2);
    assert.equal(el.pcSpy.callCount, 2);
    el.prop = false;
    assert.equal(observer.callCount, 3);
    assert.equal(el.pcSpy.callCount, 3);
    document.body.removeChild(el);
  });

  test('read only property', function() {
    class TestClass extends BaseClass {}
    TestClass.createReadOnlyProperty('prop', true);
    customElements.define('pe-read-only', TestClass);
    let el = document.createElement('pe-read-only');
    document.body.appendChild(el);
    el.prop = true;
    assert.equal(el.prop, undefined);
    assert.equal(el.pcSpy.callCount, 0);
    el._setProp(true);
    assert.equal(el.prop, true);
    assert.equal(el.pcSpy.callCount, 1);
    el.prop = false;
    assert.equal(el.prop, true);
    assert.equal(el.pcSpy.callCount, 1);
    document.body.removeChild(el);
  });

  test('reflected property', function() {
    class TestClass extends BaseClass {}
    TestClass.createReflectedProperty('prop', true);
    customElements.define('pe-reflected', TestClass);
    let el = document.createElement('pe-reflected');
    document.body.appendChild(el);
    assert.equal(el.hasAttribute('prop'), false);
    assert.equal(el.pcSpy.callCount, 0);
    el.prop = true;
    assert.equal(el.getAttribute('prop'), '');
    assert.equal(el.pcSpy.callCount, 1);
    el.prop = 'hithere';
    assert.equal(el.getAttribute('prop'), 'hithere');
    assert.equal(el.pcSpy.callCount, 2);
    el.prop = false;
    assert.equal(el.hasAttribute('prop'), false);
    assert.equal(el.pcSpy.callCount, 3);
    document.body.removeChild(el);
  });

  test('property observer', function() {
    class TestClass extends BaseClass {
      constructor() {
        super();
        this.observer = sinon.spy();
      }
    }
    TestClass.createPropertyObserver('prop', 'observer');
    customElements.define('pe-observer', TestClass);
    let el = document.createElement('pe-observer');
    document.body.appendChild(el);
    assert.equal(el.observer.callCount, 0);
    assert.equal(el.pcSpy.callCount, 0);
    el.prop = true;
    assert.equal(el.observer.callCount, 1);
    assert.equal(el.pcSpy.callCount, 1);
    el.prop = 'hithere';
    assert.equal(el.observer.callCount, 2);
    assert.equal(el.pcSpy.callCount, 2);
    el.prop = false;
    assert.equal(el.observer.callCount, 3);
    assert.equal(el.pcSpy.callCount, 3);
    document.body.removeChild(el);
  });

  test('method observer', function() {
    class TestClass extends BaseClass {
      constructor() {
        super();
        this.observer = sinon.spy();
      }
    }
    TestClass.createMethodObserver('observer(a, b.c)', true);
    customElements.define('pe-method-observer', TestClass);
    let el = document.createElement('pe-method-observer');
    document.body.appendChild(el);
    if (legacyUndefined) {
      assert.equal(el.observer.callCount, 0);
    } else {
      assert.equal(el.observer.callCount, 1);
      assert.deepEqual(el.observer.getCall(0).args, [undefined, undefined]);
    }
    assert.equal(el.pcSpy.callCount, 1);
    el.a = 'a';
    if (legacyUndefined) {
      assert.equal(el.observer.callCount, 0);
    } else {
      assert.equal(el.observer.callCount, 2);
      assert.deepEqual(el.observer.getCall(1).args, ['a', undefined]);
    }
    assert.equal(el.pcSpy.callCount, 2);
    el.b = {c: 'c'};
    if (legacyUndefined) {
      assert.equal(el.observer.callCount, 1);
      assert.deepEqual(el.observer.getCall(0).args, ['a', 'c']);
    } else {
      assert.equal(el.observer.callCount, 3);
      assert.deepEqual(el.observer.getCall(2).args, ['a', 'c']);
    }
    assert.equal(el.pcSpy.callCount, 3);
    el.setProperties({
      a: 'A',
      b: {c: 'C'}
    });
    if (legacyUndefined) {
      assert.equal(el.observer.callCount, 2);
      assert.deepEqual(el.observer.getCall(1).args, ['A', 'C']);
      assert.equal(el.pcSpy.callCount, 4);
    } else {
      assert.equal(el.observer.callCount, 4);
      assert.deepEqual(el.observer.getCall(3).args, ['A', 'C']);
      assert.equal(el.pcSpy.callCount, 4);
    }

    el.observer = sinon.spy();
    assert.equal(el.observer.callCount, 1);
    assert.deepEqual(el.observer.getCall(0).args, ['A', 'C']);
    assert.equal(el.pcSpy.callCount, 5);
    document.body.removeChild(el);
  });

  test('computed property', function() {
    class TestClass extends BaseClass {
      constructor() {
        super();
        this.compute = sinon.spy(function(a, c) { return a + c; });
      }
    }
    TestClass.createComputedProperty('prop', 'compute(a, b.c)');
    customElements.define('pe-computed', TestClass);
    let el = document.createElement('pe-computed');
    document.body.appendChild(el);
    assert.equal(el.compute.callCount, 0);
    assert.equal(el.pcSpy.callCount, 0);
    el.a = 'a';
    if (legacyUndefined) {
      assert.equal(el.compute.callCount, 0);
    } else {
      assert.equal(el.compute.callCount, 1);
      assert.deepEqual(el.compute.getCall(0).args, ['a', undefined]);
      assert.equal(el.prop, 'aundefined');
    }
    assert.equal(el.pcSpy.callCount, 1);
    el.b = {c: 'c'};
    if (legacyUndefined) {
      assert.equal(el.compute.callCount, 1);
      assert.deepEqual(el.compute.getCall(0).args, ['a', 'c']);
    } else {
      assert.equal(el.compute.callCount, 2);
      assert.deepEqual(el.compute.getCall(1).args, ['a', 'c']);
    }
    assert.equal(el.prop, 'ac');
    assert.equal(el.pcSpy.callCount, 2);
    el.setProperties({
      a: 'A',
      b: {c: 'C'}
    });
    if (legacyUndefined) {
      assert.equal(el.compute.callCount, 2);
      assert.deepEqual(el.compute.getCall(1).args, ['A', 'C']);
      assert.equal(el.pcSpy.callCount, 3);
    } else {
      assert.equal(el.compute.callCount, 3);
      assert.deepEqual(el.compute.getCall(2).args, ['A', 'C']);
      assert.equal(el.pcSpy.callCount, 3);
    }
    assert.equal(el.prop, 'AC');
    document.body.removeChild(el);
  });

  test('notifying property', function() {
    class TestClass extends BaseClass { }
    TestClass.createNotifyingProperty('prop');
    customElements.define('pe-notify', TestClass);
    let el = document.createElement('pe-notify');
    let handler = sinon.spy();
    el.addEventListener('prop-changed', handler);
    document.body.appendChild(el);
    assert.equal(handler.callCount, 0);
    assert.equal(el.pcSpy.callCount, 0);
    el.prop = 'prop';
    assert.equal(handler.callCount, 1);
    assert.equal(handler.getCall(0).args[0].detail.value, 'prop');
    assert.equal(el.pcSpy.callCount, 1);
    el.prop += '+';
    assert.equal(handler.callCount, 2);
    assert.equal(handler.getCall(1).args[0].detail.value, 'prop+');
    document.body.removeChild(el);
  });

  test('bound template', function() {
    let template = document.createElement('template');
    template.innerHTML = '<div>[[prop]]</div><button on-click="handler"></button>';
    class TestClass extends BaseClass {
      connectedCallback() {
        this.handler = sinon.spy();
        this.attachShadow({mode:'open'}).appendChild(this._stampTemplate(template));
        super.connectedCallback();
      }
    }
    TestClass.bindTemplate(template);
    customElements.define('pe-template', TestClass);
    let el = document.createElement('pe-template');
    document.body.appendChild(el);
    assert.equal(el.handler.callCount, 0);
    el.prop = 'prop';
    assert.equal(el.shadowRoot.querySelector('div').textContent, 'prop');
    el.shadowRoot.querySelector('button').click();
    assert.equal(el.handler.callCount, 1);
    document.body.removeChild(el);
  });

});

suite('warn on legacy differences', () => {

  setup(function() {
    sinon.spy(console, 'warn');
  });

  teardown(function() {
    setLegacyWarnings(false);
    console.warn.restore();
  });

  test('warn if non-declared property used in binding', () => {
    setLegacyWarnings(true);
    Polymer({
      is: 'x-warn-undeclared-binding',
      _template: html`
        <div attr$="[[undeclaredToAttr]]"
             prop="[[undeclaredToProp]]">
             [[undeclaredToText]]
             [[compute(undeclaredDep1, undeclaredDep2)]]
             </div>
        <template is="dom-repeat" items="[0]">
          <div attr$="[[nestedUndeclaredToAttr]]"
             prop="[[nestedUndeclaredToProp]]">
             [[nestedUndeclaredToText]]
             [[compute(nestedUndeclaredDep1, nestedUndeclaredDep2)]]
             </div>
        </template>
        `,
      compute(dep) { return dep; }
    });
    const el = document.createElement('x-warn-undeclared-binding');
    document.body.appendChild(el);
    flush();
    document.body.removeChild(el);
    assert.equal(console.warn.callCount, 10);
  });

  test('do not warn on valid non-property bindings', () => {
    setLegacyWarnings(true);
    Polymer({
      is: 'x-nowarn-undeclared-binding',
      properties: {
        hostProp: String,
        dynamicFn: Function
      },
      _template: html`
        <div>[[hostProp]]</div>    <!-- host prop -->
        <div>[[noDeps()]]</div>    <!-- method with no deps -->
        <div>[[literalDeps('hi')]]</div>  <!-- method with literal deps -->
        <div>[[dynamicFn()]]</div> <!-- dynamic function with no deps -->
        <div>[[dynamicFnWithDep(hostProp)]]</div>  <!-- dynamic function with deps -->
        <template is="dom-repeat" items="[0]" as="instProp">
          <div>[[instProp]]</div>  <!-- instance prop -->
          <div>[[hostProp]]</div>  <!-- nested host prop -->
          <div>[[noDeps()]]</div>  <!-- nested method with no deps -->
          <div>[[literalDeps('hi')]]</div>  <!-- method with literal deps -->
          <div>[[dynamicFn()]]</div> <!-- dynamic function with no deps -->
          <div>[[dynamicFnWithDep(hostProp)]]</div>  <!-- dynamic function with deps -->
        </template>
      `,
      noDeps() { return 'static'; },
      dynamicFn(dep) { return dep; },
      dynamicFnWithDep(dep) { return dep; }
    });
    const el = document.createElement('x-nowarn-undeclared-binding');
    document.body.appendChild(el);
    flush();
    document.body.removeChild(el);
    assert.equal(console.warn.callCount, 0);
  });

  test('warn when re-declaring a computed property', () => {
    Polymer({
      is: 'x-warn-redeclared-computed',
      behaviors: [{
        properties: {
          a: { computed: 'compute(x)' }
        }
      }, {
        properties: {
          a: { computed: 'compute(y)' }
        }
      }]
    });
    const el = document.createElement('x-warn-redeclared-computed');
    document.body.appendChild(el);
    document.body.removeChild(el);
    assert.equal(console.warn.callCount, 1);
  });

  test('warn when disabling readOnly on a readOnly property', () => {
    Polymer({
      is: 'x-warn-disable-readonly',
      behaviors: [{
        properties: {
          a: { readOnly: true }
        }
      }, {
        properties: {
          a: { readOnly: false }
        }
      }]
    });
    const el = document.createElement('x-warn-disable-readonly');
    document.body.appendChild(el);
    document.body.removeChild(el);
    assert.equal(console.warn.callCount, 1);
  });

  test('warn when disabling reflect on a reflect property', () => {
    Polymer({
      is: 'x-warn-disable-reflect',
      behaviors: [{
        properties: {
          a: { reflectToAttribute: true }
        }
      }, {
        properties: {
          a: { reflectToAttribute: false }
        }
      }]
    });
    const el = document.createElement('x-warn-disable-reflect');
    document.body.appendChild(el);
    document.body.removeChild(el);
    assert.equal(console.warn.callCount, 1);
  });

  test('warn when disabling notify on a notify property', () => {
    Polymer({
      is: 'x-warn-disable-notify',
      behaviors: [{
        properties: {
          a: { notify: true }
        }
      }, {
        properties: {
          a: { notify: false }
        }
      }]
    });
    const el = document.createElement('x-warn-disable-notify');
    document.body.appendChild(el);
    document.body.removeChild(el);
    assert.equal(console.warn.callCount, 1);
  });

});

if (orderedComputed) {

  suite('computed property ordering', function() {
    var el;

    setup(function() {
      el = document.createElement('x-computed-ordering');
      document.body.appendChild(el);
    });

    teardown(function() {
      document.body.removeChild(el);
    });

    test('computed property ordering', function() {

      assert.equal(el.bc, 110);
      assert.equal(el.bcd, 111);
      assert.equal(el.abbcd, 1211);
      assert.equal(el.computeBC.callCount, 1);
      assert.equal(el.computeBCD.callCount, 1);
      assert.equal(el.computeABBCD.callCount, 1);

      el.setProperties({
        b: 200,
        c: 20
      });
      assert.equal(el.bc, 220);
      assert.equal(el.bcd, 221);
      assert.equal(el.abbcd, 1421);
      assert.equal(el.computeBC.callCount, 2);
      assert.equal(el.computeBCD.callCount, 2);
      assert.equal(el.computeABBCD.callCount, 2);

      el.a = 2000;
      assert.equal(el.bc, 220);
      assert.equal(el.bcd, 221);
      assert.equal(el.abbcd, 2421);
      assert.equal(el.computeBC.callCount, 2);
      assert.equal(el.computeBCD.callCount, 2);
      assert.equal(el.computeABBCD.callCount, 3);

      el.c = 30;
      assert.equal(el.bc, 230);
      assert.equal(el.bcd, 231);
      assert.equal(el.abbcd, 2431);
      assert.equal(el.computeBC.callCount, 3);
      assert.equal(el.computeBCD.callCount, 3);
      assert.equal(el.computeABBCD.callCount, 4);

      el.d = 2;
      assert.equal(el.bc, 230);
      assert.equal(el.bcd, 232);
      assert.equal(el.abbcd, 2432);
      assert.equal(el.computeBC.callCount, 3);
      assert.equal(el.computeBCD.callCount, 4);
      assert.equal(el.computeABBCD.callCount, 5);

      el.setProperties({
        a: 1000,
        b: 100,
        c: 10,
        d: 1
      });
      assert.equal(el.bc, 110);
      assert.equal(el.bcd, 111);
      assert.equal(el.abbcd, 1211);
      assert.equal(el.computeBC.callCount, 4);
      assert.equal(el.computeBCD.callCount, 5);
      assert.equal(el.computeABBCD.callCount, 6);

    });

    test('computing functions as dependencies', function() {
      assert.equal(el.xy, '222');
      assert.equal(el.computeXYSpy.callCount, 1);
      assert.equal(el.xyChanged.callCount, 1);

      el.setProperties({
        x: 3,
        z: 300
      });
      assert.equal(el.xy, '323');
      assert.equal(el.computeXYSpy.callCount, 2);
      assert.equal(el.xyChanged.callCount, 2);
    });

    test('ensure circular dependencies do not iloop', function() {
      el.circIn = 1;
      // The answers here don't _really_ matter; it just shouldn't iloop
      // circA will go first (defined first), which will pull circB, which
      // has no circA yet, and thus returns 1 for this turn
      assert.ok(el.circA); // should be equal to 2, based on impl
      assert.ok(el.circB); // should be equal to 1, based on impl
    });

  });

}

</script>

</body>
</html>
